package client // Package client import "xemo.top/docker/docker/client"
import (
	"bufio"
	"context"
	"crypto/tls"
	"fmt"
	"github.com/docker/go-connections/sockets"
	"github.com/pkg/errors"
	"net"
	"net/http"
	"net/http/httputil"
	"net/url"
	"time"
	"xemo.top/docker/docker/api/types"
)

// postHijacked sends a POST request and hijacks the connection
func (cli *Client) postHijacked(ctx context.Context, path string, query url.Values, body interface{}, headers map[string][]string) (types.HijackedResponse, error) {
	bodyEncoded, err := encodeData(body)
	if err != nil {
		return types.HijackedResponse{}, err
	}
	apiPath := cli.getAPIPath(ctx, path, query)
	req, err := http.NewRequest("POST", apiPath, bodyEncoded)
	if err != nil {
		return types.HijackedResponse{}, err
	}
	req = cli.addHeaders(req, headers)
	conn, err := cli.setupHijackConn(ctx, req, "tcp")
	if err != nil {
		return types.HijackedResponse{}, err
	}
	return types.HijackedResponse{Conn: conn, Reader: bufio.NewReader(conn)}, err
}

func (cli *Client) setupHijackConn(ctx context.Context, req *http.Request, proto string) (net.Conn, error) {
	req.Host = cli.addr
	req.Header.Set("Connection", "Upgrade")
	req.Header.Set("Upgrade", proto)
	dialer := cli.Dialer()
	conn, err := dialer(ctx)
	if err != nil {
		return nil, errors.Wrap(err, "cannot connect to the Docker daemon.Is 'docker daemon' running on this host?")
	}
	// 当我们为劫持设置TCP连接时，可能会有长时间的不活动 (长时间运行的命令没有输出)，在某些网络设置中可能会导致ECONNTIMEOUT，使客户端处于未知状态。在套接字连接上设置TCP KeepAlive将禁止ECONNTIMEOUT，除非套接字连接确实断开
	// When we set up a TCP connection for hijack,there could be long periods of inactivity (a long running command with no output) that in certain network setups may cause ECONNTIMEOUT,leaving the client in an unknown state.Setting TCP KeepAlive on the socket connection will prohibit ECONNTIMEOUT unless the socket connection truly is broken
	if tcpConn, ok := conn.(*net.TCPConn); ok {
		tcpConn.SetKeepAlive(true)
		tcpConn.SetKeepAlivePeriod(30 * time.Second)
	}
	clientconn := httputil.NewClientConn(conn, nil)
	defer clientconn.Close()

	// 服务器劫持连接，预期出现错误 “连接关闭”
	// Server hijacks the connection,error `connection closed` expected
	resp, err := clientconn.Do(req)
	if err != httputil.ErrPersistEOF {
		if err != nil {
			return nil, err
		}
		if resp.StatusCode != http.StatusSwitchingProtocols {
			resp.Body.Close()
			return nil, fmt.Errorf("unable to upgrade to %s,received %d", proto, resp.StatusCode)
		}
	}
	c, br := clientconn.Hijack()
	if br.Buffered() > 0 {
		// 如果有缓冲的内容，则包装连接。我们返回一个实现CloseWrite的对象，如果底层连接实现它。
		// If there is buffered content,wrap the connection. We return an object that implements CloseWrite iff the underlying connection implements it.
		if _, ok := c.(types.CloseWriter); ok {
			c = &hijackedConnCloseWriter{&hijackedConn{c, br}}
		} else {
			c = &hijackedConn{c, br}
		}
	} else {
		br.Reset(nil)
	}
	return c, nil
}

// hijackedConn 包装一个net.Conn，并在以下情况下由setupHijackConn返回: a) 调用Hijack() 时http层中已经有缓冲数据，b) 底层net.Conn * 不 * 实现CloseWrite()。hijackedConn也不实现CloseWrite()
// hijackedConn wraps a net.Conn and is returned by setupHijackConn in the case that a) there was already buffered data in the http layer when Hijack() was called,and b) the underlying net.Conn does *not* implement CloseWrite().hijackedConn does not implement CloseWrite() either
type hijackedConn struct {
	net.Conn
	r *bufio.Reader
}

func (c *hijackedConn) Read(b []byte) (int, error) {
	return c.r.Read(b)
}

// hijackedConnCloseWriter 是另外实现CloseWrite() 的hijackedConn。它由setupHijackConn在以下情况下返回: a) 在调用Hijack() 时在http层中已经有缓冲数据，并且b) 底层net.Conn * 确实 * 实现CloseWrite()。
// hijackedConnCloseWriter is a hijackedConn which additionally implements CloseWrite().It is returned by setupHijackConn in the case that a) there was already buffered data in the  http layer when Hijack() was called,and b) the underlying net.Conn *does* implement CloseWrite().
type hijackedConnCloseWriter struct {
	*hijackedConn
}

var _ types.CloseWriter = &hijackedConnCloseWriter{}

func (c *hijackedConnCloseWriter) CloseWrite() error {
	conn := c.Conn.(types.CloseWriter)
	return conn.CloseWrite()
}

// fallbackDial is used when WithDialer() was not called.See cli.Dialer()
func fallbackDial(proto, addr string, tlsConfig *tls.Config) (net.Conn, error) {
	if tlsConfig != nil && proto != "unix" && proto != "npipe" {
		return tls.Dial(proto, addr, tlsConfig)
	}
	if proto == "npipe" {
		return sockets.DialPipe(addr, 32*time.Second)
	}
	return net.Dial(proto, addr)
}
